package com.google.code.sms.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;

import org.apache.commons.io.IOUtils;

import com.google.code.sms.Logger;
import com.google.code.sms.dao.MediaFolderDao;
import com.google.code.sms.dao.UserDao;
import com.google.code.sms.domain.MediaFolder;
import com.google.code.sms.domain.Theme;
import com.google.code.sms.util.StringUtil;
import com.google.code.sms.util.Util;

/**
 * Provides persistent storage of application settings and preferences.
 *
 * @author Scott Ware
 */
public class SettingsService
{
    // SMS Version
    private static final String SMS_VERSION = "0.1";
    
    // SMS home directory
    private static final File SMS_HOME = new File("/var/sms");
    // Global settings.
    private static final String KEY_INDEX_STRING = "IndexString";
    private static final String KEY_IGNORED_ARTICLES = "IgnoredArticles";
    private static final String KEY_PLAYLIST_FOLDER = "PlaylistFolder";
    private static final String KEY_MUSIC_FILE_TYPES = "MusicFileTypes";
    private static final String KEY_VIDEO_FILE_TYPES = "VideoFileTypes";
    private static final String KEY_COVER_ART_FILE_TYPES = "CoverArtFileTypes";
    private static final String KEY_POSTER_FILE_TYPES = "PosterFileTypes";
    private static final String KEY_SUBTITLES_FILE_TYPES = "SubtitlesFileTypes";
    private static final String KEY_LOCALE_LANGUAGE = "LocaleLanguage";
    private static final String KEY_LOCALE_COUNTRY = "LocaleCountry";
    private static final String KEY_LOCALE_VARIANT = "LocaleVariant";
    private static final String KEY_THEME_ID = "Theme";
    private static final String KEY_DOWNLOAD_BITRATE_LIMIT = "DownloadBitrateLimit";
    private static final String KEY_STREAM_PORT = "StreamPort";
    private static final String KEY_DOWNSAMPLING_COMMAND = "DownsamplingCommand3";
    private static final String KEY_HLS_COMMAND = "HlsCommand2";
    private static final String KEY_REWRITE_URL = "RewriteUrl";
    private static final String KEY_PORT_FORWARDING_ENABLED = "PortForwardingEnabled";
    private static final String KEY_PORT = "Port";
    private static final String KEY_URL_REDIRECTION_ENABLED = "UrlRedirectionEnabled";
    private static final String KEY_URL_REDIRECT_FROM = "UrlRedirectFrom";
    private static final String KEY_URL_REDIRECT_CONTEXT_PATH = "UrlRedirectContextPath";
    private static final String KEY_SERVER_ID = "ServerId";
    private static final String KEY_SETTINGS_CHANGED = "SettingsChanged";
    private static final String KEY_LAST_SCANNED = "LastScanned";
    private static final String KEY_ORGANIZE_BY_FOLDER_STRUCTURE = "OrganizeByFolderStructure";
    // Default values.
    private static final String DEFAULT_INDEX_STRING = "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ)";
    private static final String DEFAULT_IGNORED_ARTICLES = "The El La Los Las Le Les";
    private static final String DEFAULT_PLAYLIST_FOLDER = Util.getDefaultPlaylistFolder();
    private static final String DEFAULT_MUSIC_FILE_TYPES = "mp3 ogg oga aac m4a flac wav wma aif aiff ape mpc shn";
    private static final String DEFAULT_VIDEO_FILE_TYPES = "flv avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts";
    private static final String DEFAULT_COVER_ART_FILE_TYPES = "cover.jpg folder.jpg";
    private static final String DEFAULT_POSTER_FILE_TYPES = "fanart.jpg";
    private static final String DEFAULT_SUBTITLES_FILE_TYPES = "forced.vtt forced.srt";
    private static final String DEFAULT_LOCALE_LANGUAGE = "en_GB";
    private static final String DEFAULT_LOCALE_COUNTRY = "";
    private static final String DEFAULT_LOCALE_VARIANT = "";
    private static final String DEFAULT_THEME_ID = "default";
    private static final long DEFAULT_DOWNLOAD_BITRATE_LIMIT = 0;
    private static final long DEFAULT_STREAM_PORT = 0;
    private static final String DEFAULT_DOWNSAMPLING_COMMAND = "ffmpeg -i %s -ab %bk -v 0 -f mp3 -";
    private static final String DEFAULT_HLS_COMMAND = "ffmpeg -ss %o -t %d -i %s -async 1 -b %bk -s %wx%h -ar 44100 -ac 2 -v 0 -f mpegts -vcodec libx264 -preset superfast -acodec libmp3lame -threads 0 -";
    private static final boolean DEFAULT_REWRITE_URL = true;
    private static final boolean DEFAULT_PORT_FORWARDING_ENABLED = false;
    private static final int DEFAULT_PORT = 80;
    private static final boolean DEFAULT_URL_REDIRECTION_ENABLED = false;
    private static final String DEFAULT_URL_REDIRECT_FROM = "yourname";
    private static final String DEFAULT_URL_REDIRECT_CONTEXT_PATH = null;
    private static final String DEFAULT_SERVER_ID = null;
    private static final long DEFAULT_SETTINGS_CHANGED = 0L;
    private static final boolean DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE = true;
    // Array of obsolete keys.  Used to clean property file.
    private static final List<String> OBSOLETE_KEYS = Arrays.asList("PortForwardingPublicPort", "PortForwardingLocalPort",
            "DownsamplingCommand", "DownsamplingCommand2", "AutoCoverBatch", "MusicMask", "VideoMask", "CoverArtMask, HlsCommand");
    private static final String LOCALES_FILE = "/com/google/code/sms/i18n/locales.txt";
    private static final String THEMES_FILE = "/com/google/code/sms/theme/themes.txt";
    private static final Logger LOG = Logger.getLogger("SettingsService");
    private Properties properties = new Properties();
    private List<Theme> themes;
    private List<Locale> locales;
    private MediaFolderDao mediaFolderDao;
    private String[] cachedCoverArtFileTypesArray;
    private String[] cachedPosterFileTypesArray;
    private String[] cachedSubtitlesFileTypesArray;
    private String[] cachedMusicFileTypesArray;
    private String[] cachedVideoFileTypesArray;
    private List<MediaFolder> cachedMediaFolders;
    private static File smsHome;

    public SettingsService() {
        File propertyFile = getPropertyFile();

        if (propertyFile.exists()) {
            FileInputStream in = null;
            try {
                in = new FileInputStream(propertyFile);
                properties.load(in);
            } catch (Exception x) {
                LOG.error("Unable to read from property file.", x);
            } finally {
                IOUtils.closeQuietly(in);
            }

            // Remove obsolete properties.
            for (Iterator<Object> iterator = properties.keySet().iterator(); iterator.hasNext();) {
                String key = (String) iterator.next();
                if (OBSOLETE_KEYS.contains(key)) {
                    LOG.debug("Removing obsolete property [" + key + ']');
                    iterator.remove();
                }
            }
        }

        save(false);
    }
    
    public void save() {
        save(true);
    }

    public final void save(boolean updateChangedDate) {
        if (updateChangedDate) {
            setProperty(KEY_SETTINGS_CHANGED, String.valueOf(System.currentTimeMillis()));
        }

        OutputStream out = null;
        try {
            out = new FileOutputStream(getPropertyFile());
            properties.store(out, "SMS preferences.  NOTE: This file is automatically generated.");
        } catch (Exception x) {
            LOG.error("Unable to write to property file.", x);
        } finally {
            IOUtils.closeQuietly(out);
        }
    }

    private File getPropertyFile() {
        return new File(getSMSHome(), "sms.properties");
    }

    /**
     * Returns the SMS home directory.
     *
     * @return The SMS home directory, if it exists.
     * @throws RuntimeException If directory doesn't exist.
     */
    public static synchronized File getSMSHome() {

        if (smsHome != null) {
            return smsHome;
        }

        File home;

        String overrideHome = System.getProperty("sms.home");
        if (overrideHome != null) {
            home = new File(overrideHome);
        } else {
            home = SMS_HOME;
        }

        // Attempt to create home directory if it doesn't exist.
        if (!home.exists() || !home.isDirectory()) {
            boolean success = home.mkdirs();
            if (success) {
                smsHome = home;
            } else {
                String message = "The directory " + home + " does not exist. Please create it and make it writable. "
                        + "(You can override the directory location by specifying -Dsms.home=... when "
                        + "starting the servlet container.)";
                System.err.println("ERROR: " + message);
            }
        } else {
            smsHome = home;
        }

        return home;
    }

    private boolean getBoolean(String key, boolean defaultValue) {
        return Boolean.valueOf(properties.getProperty(key, String.valueOf(defaultValue)));
    }

    private void setBoolean(String key, boolean value) {
        setProperty(key, String.valueOf(value));
    }

    public String getIndexString() {
        return properties.getProperty(KEY_INDEX_STRING, DEFAULT_INDEX_STRING);
    }

    public void setIndexString(String indexString) {
        setProperty(KEY_INDEX_STRING, indexString);
    }

    public String getIgnoredArticles() {
        return properties.getProperty(KEY_IGNORED_ARTICLES, DEFAULT_IGNORED_ARTICLES);
    }

    public String[] getIgnoredArticlesAsArray() {
        return getIgnoredArticles().split("\\s+");
    }

    public void setIgnoredArticles(String ignoredArticles) {
        setProperty(KEY_IGNORED_ARTICLES, ignoredArticles);
    }

    public String getPlaylistFolder() {
        return properties.getProperty(KEY_PLAYLIST_FOLDER, DEFAULT_PLAYLIST_FOLDER);
    }

    public void setPlaylistFolder(String playlistFolder) {
        setProperty(KEY_PLAYLIST_FOLDER, playlistFolder);
    }

    public String getMusicFileTypes() {
        return properties.getProperty(KEY_MUSIC_FILE_TYPES, DEFAULT_MUSIC_FILE_TYPES);
    }

    public synchronized void setMusicFileTypes(String fileTypes) {
        setProperty(KEY_MUSIC_FILE_TYPES, fileTypes);
        cachedMusicFileTypesArray = null;
    }

    public synchronized String[] getMusicFileTypesAsArray() {
        if (cachedMusicFileTypesArray == null) {
            cachedMusicFileTypesArray = toStringArray(getMusicFileTypes());
        }
        return cachedMusicFileTypesArray;
    }

    public String getVideoFileTypes() {
        return properties.getProperty(KEY_VIDEO_FILE_TYPES, DEFAULT_VIDEO_FILE_TYPES);
    }

    public synchronized void setVideoFileTypes(String fileTypes) {
        setProperty(KEY_VIDEO_FILE_TYPES, fileTypes);
        cachedVideoFileTypesArray = null;
    }

    public synchronized String[] getVideoFileTypesAsArray() {
        if (cachedVideoFileTypesArray == null) {
            cachedVideoFileTypesArray = toStringArray(getVideoFileTypes());
        }
        return cachedVideoFileTypesArray;
    }

    public String getCoverArtFileTypes() {
        return properties.getProperty(KEY_COVER_ART_FILE_TYPES, DEFAULT_COVER_ART_FILE_TYPES);
    }

    public synchronized void setCoverArtFileTypes(String fileTypes) {
        setProperty(KEY_COVER_ART_FILE_TYPES, fileTypes);
        cachedCoverArtFileTypesArray = null;
    }

    public synchronized String[] getCoverArtFileTypesAsArray() {
        if (cachedCoverArtFileTypesArray == null) {
            cachedCoverArtFileTypesArray = toStringArray(getCoverArtFileTypes());
        }
        return cachedCoverArtFileTypesArray;
    }
    
    public String getPosterFileTypes() {
        return properties.getProperty(KEY_POSTER_FILE_TYPES, DEFAULT_POSTER_FILE_TYPES);
    }

    public synchronized void setPosterFileTypes(String fileTypes) {
        setProperty(KEY_POSTER_FILE_TYPES, fileTypes);
        cachedPosterFileTypesArray = null;
    }

    public synchronized String[] getPosterFileTypesAsArray() {
        if (cachedPosterFileTypesArray == null) {
            cachedPosterFileTypesArray = toStringArray(getPosterFileTypes());
        }
        return cachedPosterFileTypesArray;
    }
    
    public String getSubtitlesFileTypes() {
        return properties.getProperty(KEY_SUBTITLES_FILE_TYPES, DEFAULT_SUBTITLES_FILE_TYPES);
    }

    public synchronized void setSubtitlesFileTypes(String fileTypes) {
        setProperty(KEY_SUBTITLES_FILE_TYPES, fileTypes);
        cachedSubtitlesFileTypesArray = null;
    }

    public synchronized String[] getSubtitlesFileTypesAsArray() {
        if (cachedSubtitlesFileTypesArray == null) {
            cachedSubtitlesFileTypesArray = toStringArray(getSubtitlesFileTypes());
        }
        return cachedSubtitlesFileTypesArray;
    }

    /**
     * @return The download bitrate limit in Kbit/s. Zero if unlimited.
     */
    public long getDownloadBitrateLimit() {
        return Long.parseLong(properties.getProperty(KEY_DOWNLOAD_BITRATE_LIMIT, "" + DEFAULT_DOWNLOAD_BITRATE_LIMIT));
    }

    /**
     * @param limit The download bitrate limit in Kbit/s. Zero if unlimited.
     */
    public void setDownloadBitrateLimit(long limit) {
        setProperty(KEY_DOWNLOAD_BITRATE_LIMIT, "" + limit);
    }

    /**
     * @return The non-SSL stream port. Zero if disabled.
     */
    public int getStreamPort() {
        return Integer.parseInt(properties.getProperty(KEY_STREAM_PORT, "" + DEFAULT_STREAM_PORT));
    }

    /**
     * @param port The non-SSL stream port. Zero if disabled.
     */
    public void setStreamPort(int port) {
        setProperty(KEY_STREAM_PORT, "" + port);
    }

    public String getDownsamplingCommand() {
        return properties.getProperty(KEY_DOWNSAMPLING_COMMAND, DEFAULT_DOWNSAMPLING_COMMAND);
    }

    public void setDownsamplingCommand(String command) {
        setProperty(KEY_DOWNSAMPLING_COMMAND, command);
    }

    public String getHlsCommand() {
        return properties.getProperty(KEY_HLS_COMMAND, DEFAULT_HLS_COMMAND);
    }

    public void setHlsCommand(String command) {
        setProperty(KEY_HLS_COMMAND, command);
    }

    public boolean isRewriteUrlEnabled() {
        return getBoolean(KEY_REWRITE_URL, DEFAULT_REWRITE_URL);
    }

    public void setRewriteUrlEnabled(boolean rewriteUrl) {
        setBoolean(KEY_REWRITE_URL, rewriteUrl);
    }

    public boolean isPortForwardingEnabled() {
        return getBoolean(KEY_PORT_FORWARDING_ENABLED, DEFAULT_PORT_FORWARDING_ENABLED);
    }

    public void setPortForwardingEnabled(boolean isPortForwardingEnabled) {
        setBoolean(KEY_PORT_FORWARDING_ENABLED, isPortForwardingEnabled);
    }

    public int getPort() {
        return Integer.valueOf(properties.getProperty(KEY_PORT, String.valueOf(DEFAULT_PORT)));
    }

    public void setPort(int port) {
        setProperty(KEY_PORT, String.valueOf(port));
    }

    public boolean isUrlRedirectionEnabled() {
        return getBoolean(KEY_URL_REDIRECTION_ENABLED, DEFAULT_URL_REDIRECTION_ENABLED);
    }

    public void setUrlRedirectionEnabled(boolean isUrlRedirectionEnabled) {
        setBoolean(KEY_URL_REDIRECTION_ENABLED, isUrlRedirectionEnabled);
    }

    public String getUrlRedirectFrom() {
        return properties.getProperty(KEY_URL_REDIRECT_FROM, DEFAULT_URL_REDIRECT_FROM);
    }

    public void setUrlRedirectFrom(String urlRedirectFrom) {
        properties.setProperty(KEY_URL_REDIRECT_FROM, urlRedirectFrom);
    }

    public String getUrlRedirectContextPath() {
        return properties.getProperty(KEY_URL_REDIRECT_CONTEXT_PATH, DEFAULT_URL_REDIRECT_CONTEXT_PATH);
    }

    public void setUrlRedirectContextPath(String contextPath) {
        properties.setProperty(KEY_URL_REDIRECT_CONTEXT_PATH, contextPath);
    }

    public String getServerId() {
        return properties.getProperty(KEY_SERVER_ID, DEFAULT_SERVER_ID);
    }

    public void setServerId(String serverId) {
        properties.setProperty(KEY_SERVER_ID, serverId);
    }

    public long getSettingsChanged() {
        return Long.parseLong(properties.getProperty(KEY_SETTINGS_CHANGED, String.valueOf(DEFAULT_SETTINGS_CHANGED)));
    }

    public Date getLastScanned() {
        String lastScanned = properties.getProperty(KEY_LAST_SCANNED);
        return lastScanned == null ? null : new Date(Long.parseLong(lastScanned));
    }

    public void setLastScanned(Date date) {
        if (date == null) {
            properties.remove(KEY_LAST_SCANNED);
        } else {
            properties.setProperty(KEY_LAST_SCANNED, String.valueOf(date.getTime()));
        }
    }

    public boolean isOrganizeByFolderStructure() {
        return getBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE);
    }

    public void setOrganizeByFolderStructure(boolean b) {
        setBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, b);
    }

    public String getVersion()
    {
        return SMS_VERSION;
    }
    
    /**
     * Returns the locale (for language, date format etc).
     *
     * @return The locale.
     */
    public Locale getLocale() {
        String language = properties.getProperty(KEY_LOCALE_LANGUAGE, DEFAULT_LOCALE_LANGUAGE);
        String country = properties.getProperty(KEY_LOCALE_COUNTRY, DEFAULT_LOCALE_COUNTRY);
        String variant = properties.getProperty(KEY_LOCALE_VARIANT, DEFAULT_LOCALE_VARIANT);

        return new Locale(language, country, variant);
    }

    /**
     * Sets the locale (for language, date format etc.)
     *
     * @param locale The locale.
     */
    public void setLocale(Locale locale) {
        setProperty(KEY_LOCALE_LANGUAGE, locale.getLanguage());
        setProperty(KEY_LOCALE_COUNTRY, locale.getCountry());
        setProperty(KEY_LOCALE_VARIANT, locale.getVariant());
    }

    /**
     * Returns the ID of the theme to use.
     *
     * @return The theme ID.
     */
    public String getThemeId() {
        return properties.getProperty(KEY_THEME_ID, DEFAULT_THEME_ID);
    }

    /**
     * Sets the ID of the theme to use.
     *
     * @param themeId The theme ID
     */
    public void setThemeId(String themeId) {
        setProperty(KEY_THEME_ID, themeId);
    }

    /**
     * Returns a list of available themes.
     *
     * @return A list of available themes.
     */
    public synchronized Theme[] getAvailableThemes() {
        if (themes == null) {
            themes = new ArrayList<>();
            try {
                InputStream in = SettingsService.class.getResourceAsStream(THEMES_FILE);
                String[] lines = StringUtil.readLines(in);
                for (String line : lines) {
                    String[] elements = StringUtil.split(line);
                    if (elements.length == 2) {
                        themes.add(new Theme(elements[0], elements[1]));
                    } else {
                        LOG.warn("Failed to parse theme from line: [" + line + "].");
                    }
                }
            } catch (IOException x) {
                LOG.error("Failed to resolve list of themes.", x);
                themes.add(new Theme("default", "SMS default"));
            }
        }
        return themes.toArray(new Theme[themes.size()]);
    }

    /**
     * Returns a list of available locales.
     *
     * @return A list of available locales.
     */
    public synchronized Locale[] getAvailableLocales() {
        if (locales == null) {
            locales = new ArrayList<>();
            try {
                InputStream in = SettingsService.class.getResourceAsStream(LOCALES_FILE);
                String[] lines = StringUtil.readLines(in);

                for (String line : lines) {
                    locales.add(parseLocale(line));
                }

            } catch (IOException x) {
                LOG.error("Failed to resolve list of locales.", x);
                locales.add(Locale.ENGLISH);
            }
        }
        return locales.toArray(new Locale[locales.size()]);
    }

    private Locale parseLocale(String line) {
        String[] s = line.split("_");
        String language = s[0];
        String country = "";
        String variant = "";

        if (s.length > 1) {
            country = s[1];
        }
        if (s.length > 2) {
            variant = s[2];
        }
        return new Locale(language, country, variant);
    }

    /**
     * Returns all media folders. Non-existing and disabled folders are not
     * included.
     *
     * @return Possibly empty list of all media folders.
     */
    public List<MediaFolder> getAllMediaFolders() {
        return getAllMediaFolders(false, false);
    }

    /**
     * Returns all media folders.
     *
     * @param includeDisabled Whether to include disabled folders.
     * @param includeNonExisting Whether to include non-existing folders.
     * @return Possibly empty list of all media folders.
     */
    public List<MediaFolder> getAllMediaFolders(boolean includeDisabled, boolean includeNonExisting) {
        if (cachedMediaFolders == null) {
            cachedMediaFolders = mediaFolderDao.getAllMediaFolders();
        }

        List<MediaFolder> result = new ArrayList<>(cachedMediaFolders.size());
        for (MediaFolder folder : cachedMediaFolders) {
            if ((includeDisabled || folder.isEnabled()) && (includeNonExisting || folder.getFile().exists())) {
                result.add(folder);
            }
        }
        return result;
    }

    /**
     * Returns the media folder with the given ID.
     *
     * @param id The ID.
     * @return The media folder with the given ID, or <code>null</code> if not
     * found.
     */
    public MediaFolder getMediaFolderById(Integer id) {
        List<MediaFolder> all = getAllMediaFolders();
        for (MediaFolder folder : all) {
            if (id.equals(folder.getId())) {
                return folder;
            }
        }
        return null;
    }

    /**
     * Creates a new media folder.
     *
     * @param mediaFolder The media folder to create.
     */
    public void createMediaFolder(MediaFolder mediaFolder) {
        mediaFolderDao.createMediaFolder(mediaFolder);
        cachedMediaFolders = null;
    }

    /**
     * Deletes the media folder with the given ID.
     *
     * @param id The ID of the media folder to delete.
     */
    public void deleteMediaFolder(Integer id) {
        mediaFolderDao.deleteMediaFolder(id);
        cachedMediaFolders = null;
    }

    /**
     * Updates the given media folder.
     *
     * @param mediaFolder The media folder to update.
     */
    public void updateMediaFolder(MediaFolder mediaFolder) {
        mediaFolderDao.updateMediaFolder(mediaFolder);
        cachedMediaFolders = null;
    }
    
    /**
     * Check if a given path relates to a Media Folder
     * 
     * @param path The path to be checked against Media Folders.
     * @return Whether the given path relates to a Media Folder.
     */
    public boolean isMediaFolder(String path) 
    {
        // Get Setting Service & Media Folders
        List<MediaFolder> folders = getAllMediaFolders();
        
        // Recursively check Media Folder paths against given path
        for (MediaFolder folder : folders) 
        {
            if (path.equals(folder.getPath())) 
            {
                return true;
            }
        }
        
        return false;
    }

    private void setProperty(String key, String value) {
        if (value == null) {
            properties.remove(key);
        } else {
            properties.setProperty(key, value);
        }
    }

    private String[] toStringArray(String s) {
        List<String> result = new ArrayList<>();
        StringTokenizer tokenizer = new StringTokenizer(s, " ");
        while (tokenizer.hasMoreTokens()) {
            result.add(tokenizer.nextToken());
        }

        return result.toArray(new String[result.size()]);
    }

    public void setMediaFolderDao(MediaFolderDao mediaFolderDao) {
        this.mediaFolderDao = mediaFolderDao;
    }
}
